import { Plugin } from "@html_editor/plugin";
import { getCSSVariableValue, getHtmlStyle } from "@html_editor/utils/formatting";
import { withSequence } from "@html_editor/utils/resource";
import { ThemeAdvancedOption } from "./theme_advanced_option";
import { ThemeButtonOption } from "./theme_button_option";
import { ThemeColorsOption } from "./theme_colors_option";
import { ThemeHeadingsOption } from "./theme_headings_option";
import { setBuilderCSSVariables } from "@html_builder/utils/utils_css";
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import {
    convertCSSColorToRgba,
    convertRgbaToCSSColor,
    convertHslToRgb,
    convertRgbToHsl,
} from "@web/core/utils/colors";
import { reactive } from "@odoo/owl";
import { BuilderAction } from "@html_builder/core/builder_action";
import { CustomizeWebsiteVariableAction } from "../customize_website_plugin";
import { EditHeadBodyDialog } from "@website/components/edit_head_body_dialog/edit_head_body_dialog";
import { BaseOptionComponent } from "@html_builder/core/utils";

/**
 * @typedef { Object } ThemeTabShared
 * @property { ThemeTabPlugin['buildGray'] } buildGray
 * @property { ThemeTabPlugin['getGrays'] } getGrays
 * @property { ThemeTabPlugin['getGrayParams'] } getGrayParams
 * @property { ThemeTabPlugin['setGrays'] } setGrays
 * @property { ThemeTabPlugin['setGrayParams'] } setGrayParams
 */

/**
 * @typedef {import("@html_builder/core/builder_options_plugin").BuilderOptionContainer[]} theme_options
 */

export const GRAY_PARAMS = {
    EXTRA_SATURATION: "gray-extra-saturation",
    HUE: "gray-hue",
};

export const OPTION_POSITIONS = {
    COLORS: 10,
    SETTINGS: 20,
    PARAGRAPH: 30,
    HEADINGS: 40,
    BUTTON: 50,
    LINK: 60,
    INPUT: 70,
    ADVANCED: 80,
};

export class ThemeTabPlugin extends Plugin {
    static id = "themeTab";
    static shared = ["getGrayParams", "getGrays", "setGrays", "setGrayParams", "buildGray"];
    grayParams = {};
    grays = reactive({});

    /** @type {import("plugins").WebsiteResources} */
    resources = {
        builder_actions: {
            CustomizeGrayAction,
            ChangeColorPaletteAction,
            EditCustomCodeAction,
            ConfigureApiKeyAction,
            CustomizePageLayout,
        },
        theme_options: [
            withSequence(
                OPTION_POSITIONS.COLORS,
                this.getThemeOptionBlock("theme-colors", _t("Colors"), ThemeColorsOption)
            ),
            withSequence(
                OPTION_POSITIONS.SETTINGS,
                this.getThemeOptionBlock(
                    "website-settings",
                    _t("Website"),
                    class ThemeWebsiteSettingsOption extends BaseOptionComponent {
                        static template = "website.ThemeWebsiteSettingsOption";
                    }
                )
            ),
            withSequence(
                OPTION_POSITIONS.PARAGRAPH,
                this.getThemeOptionBlock(
                    "theme-paragraph",
                    _t("Paragraph"),
                    class ThemeParagraphOption extends BaseOptionComponent {
                        static template = "website.ThemeParagraphOption";
                    }
                )
            ),
            withSequence(
                OPTION_POSITIONS.HEADINGS,
                this.getThemeOptionBlock("theme-headings", _t("Headings"), ThemeHeadingsOption)
            ),
            withSequence(
                OPTION_POSITIONS.BUTTON,
                this.getThemeOptionBlock("theme-button", _t("Button"), ThemeButtonOption)
            ),
            withSequence(
                OPTION_POSITIONS.LINK,
                this.getThemeOptionBlock(
                    "theme-link",
                    _t("Link"),
                    class ThemeLinkOption extends BaseOptionComponent {
                        static template = "website.ThemeLinkOption";
                    }
                )
            ),
            withSequence(
                OPTION_POSITIONS.INPUT,
                this.getThemeOptionBlock(
                    "theme-input",
                    _t("Input Fields"),
                    class ThemeInputOption extends BaseOptionComponent {
                        static template = "website.ThemeInputOption";
                    }
                )
            ),
            withSequence(
                OPTION_POSITIONS.ADVANCED,
                this.getThemeOptionBlock("theme-advanced", _t("Advanced"), ThemeAdvancedOption)
            ),
        ],
    };

    setup() {
        // If the gray palette has been generated by Odoo standard option,
        // the hue of all gray is the same and the saturation has been
        // increased/decreased by the same amount for all grays in
        // comparaison with BS grays. However the system supports any
        // gray palette.

        const hues = [];
        const saturationDiffs = [];
        let oneHasNoSaturation = false;
        const style = this.window.getComputedStyle(this.document.body);
        const baseStyle = getComputedStyle(document.body);
        for (let id = 100; id <= 900; id += 100) {
            const gray = getCSSVariableValue(`${id}`, style);
            this.grays[id] = gray;
            const grayRGB = convertCSSColorToRgba(gray);
            const grayHSL = convertRgbToHsl(grayRGB.red, grayRGB.green, grayRGB.blue);

            const baseGray = getCSSVariableValue(`base-${id}`, baseStyle);
            const baseGrayRGB = convertCSSColorToRgba(baseGray);
            const baseGrayHSL = convertRgbToHsl(
                baseGrayRGB.red,
                baseGrayRGB.green,
                baseGrayRGB.blue
            );

            if (grayHSL.saturation > 0.01) {
                if (grayHSL.lightness > 0.01 && grayHSL.lightness < 99.99) {
                    hues.push(grayHSL.hue);
                }
                if (grayHSL.saturation < 99.99) {
                    saturationDiffs.push(grayHSL.saturation - baseGrayHSL.saturation);
                }
            } else {
                oneHasNoSaturation = true;
            }
        }
        this.grayHueIsDefined = !!hues.length;

        // Average of angles: we need to take the average of found hues
        // because even if grays are supposed to be set to the exact
        // same hue by the Odoo editor, there might be rounding errors
        // during the conversion from RGB to HSL as the HSL system
        // allows to represent more colors that the RGB hexadecimal
        // notation (also: hue 360 = hue 0 and should not be averaged to 180).
        // This also better support random gray palettes.
        this.grayParams[GRAY_PARAMS.HUE] = !hues.length
            ? 0
            : Math.round(
                  (Math.atan2(
                      hues
                          .map((hue) => Math.sin((hue * Math.PI) / 180))
                          .reduce((memo, value) => memo + value, 0) / hues.length,
                      hues
                          .map((hue) => Math.cos((hue * Math.PI) / 180))
                          .reduce((memo, value) => memo + value, 0) / hues.length
                  ) *
                      180) /
                      Math.PI +
                      360
              ) % 360;

        // Average of found saturation diffs, or all grays have no
        // saturation, or all grays are fully saturated.
        this.grayParams[GRAY_PARAMS.EXTRA_SATURATION] = saturationDiffs.length
            ? saturationDiffs.reduce((memo, value) => memo + value, 0) / saturationDiffs.length
            : oneHasNoSaturation
            ? -100
            : 100;
    }
    getGrayParams() {
        return this.grayParams;
    }
    getGrays() {
        return this.grays;
    }
    setGrayParams(key, value) {
        this.grayParams[key] = value;
    }
    setGrays(key, value) {
        this.grays[key] = value;
    }
    buildGray(id) {
        // Getting base grays defined in color_palette.scss
        const gray = getCSSVariableValue(`base-${id}`, getComputedStyle(document.documentElement));
        const grayRGB = convertCSSColorToRgba(gray);
        const hsl = convertRgbToHsl(grayRGB.red, grayRGB.green, grayRGB.blue);
        const adjustedGrayRGB = convertHslToRgb(
            this.grayParams[GRAY_PARAMS.HUE],
            Math.min(
                Math.max(hsl.saturation + this.grayParams[GRAY_PARAMS.EXTRA_SATURATION], 0),
                100
            ),
            hsl.lightness
        );
        return convertRgbaToCSSColor(
            adjustedGrayRGB.red,
            adjustedGrayRGB.green,
            adjustedGrayRGB.blue
        );
    }

    getThemeOptionBlock(id, name, options) {
        // TODO Have a specific kind of options container that takes the specific parameters like name, no element, no selector...
        const el = this.document.createElement("div");
        el.dataset.name = name;
        this.document.body.appendChild(el); // Currently editingElement needs to be isConnected

        options.selector = "*";

        return {
            id: id,
            element: el,
            hasOverlayOptions: false,
            headerMiddleButton: false,
            isClonable: false,
            isRemovable: false,
            options: [options],
            optionsContainerTopButtons: [],
            snippetModel: {},
        };
    }
}

export class CustomizeGrayAction extends BuilderAction {
    static id = "customizeGray";
    static dependencies = ["customizeWebsite", "themeTab"];
    setup() {
        this.preview = false;
        this.dependencies.customizeWebsite.withCustomHistory(this);
    }
    getValue({ params: { mainParam: grayParamName } }) {
        return this.dependencies.themeTab.getGrayParams()[grayParamName];
    }
    async apply({ params: { mainParam: grayParamName }, value }) {
        // Gray parameters are used *on the JS side* to compute the grays that
        // will be saved in the database. We indeed need those grays to be
        // computed here for faster previews so this allows to not duplicate
        // most of the logic. Also, this gives flexibility to maybe allow full
        // customization of grays in custo and themes. Also, this allows to ease
        // migration if the computation here was to change: the user grays would
        // still be unchanged as saved in the database.

        this.dependencies.themeTab.setGrayParams(grayParamName, parseInt(value));
        for (let i = 1; i < 10; i++) {
            const key = (100 * i).toString();
            this.dependencies.themeTab.setGrays(key, this.dependencies.themeTab.buildGray(key));
        }

        // Save all computed (JS side) grays in database
        await this.dependencies.customizeWebsite.customizeWebsiteColors(
            this.dependencies.themeTab.getGrays(),
            {
                colorType: "gray",
            }
        );
    }
}
export class ChangeColorPaletteAction extends CustomizeWebsiteVariableAction {
    static id = "changeColorPalette";
    static dependencies = ["customizeWebsite"];
    setup() {
        this.preview = false;
        this.dependencies.customizeWebsite.withCustomHistory(this);
    }
    async load() {
        const style = this.window.getComputedStyle(this.document.body);
        const hasCustomizedColors = getCSSVariableValue("has-customized-colors", style);
        if (hasCustomizedColors && hasCustomizedColors !== "false") {
            return new Promise((resolve) => {
                this.services.dialog.add(ConfirmationDialog, {
                    body: _t(
                        "Changing the color palette will reset all your color customizations, are you sure you want to proceed?"
                    ),
                    confirm: () => resolve(true),
                    cancel: () => resolve(false),
                });
            });
        }
        return true;
    }
    async apply(context) {
        if (!context.loadResult) {
            return;
        }
        await super.apply(context);
        setBuilderCSSVariables(getHtmlStyle(this.document));
    }
}

export class EditCustomCodeAction extends BuilderAction {
    static id = "editCustomCode";
    apply() {
        this.services.dialog.add(EditHeadBodyDialog);
    }
}

export class ConfigureApiKeyAction extends BuilderAction {
    static id = "configureApiKey";
    static dependencies = ["googleMapsOption"];
    apply() {
        this.dependencies.googleMapsOption.configureGMapsAPI("", true);
    }
}

export class CustomizePageLayout extends CustomizeWebsiteVariableAction {
    static id = "customizePageLayout";

    async apply(...args) {
        await super.apply(...args);
        // since we do not reload on website variables customization we need to
        // trigger resize to have navbar layout recomputed when we modify page layout
        this.window.dispatchEvent(new Event("resize"));
    }
}

registry.category("website-plugins").add(ThemeTabPlugin.id, ThemeTabPlugin);
